﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;

using Effects.Wave;
using Microsoft.DirectX.DirectSound;

namespace Effects.GUI
{
    
    /// <summary>
    ///  Implements <see cref="AudioPlayer"/> interface using Managed DirectSound library and <see cref="EffectStream"/> class to apply effects.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   Main idea is to use small circular buffer as source for DirectSound.SecondaryBuffer. 
    ///  </para>
    ///  <para>
    ///   SecondBuffer is set to play looping the data from array and timer (ticks every 10 miliseconds or so) puts new data into this array. Result is
    ///   that SecondaryBuffer plays whole file without knowing it and there is know need to have data to much before they are played.
    ///  </para>
    ///  
    /// </remarks>
    class EffectStreamPlayer : AudioPlayer
    {
        #region input
        // input file and effect stream stuff

        // Wave instace to be played
        /// <summary>
        ///  <see cref="Wave"/> that is played.
        /// </summary>
        private Wave.Wave wav;

        /// <summary>
        ///  Converts <see cref="FmtChunk"/> to DirectSound.WaveFormat.
        /// </summary>
        /// <param name="fmt"> Fmt chunk describing wave format </param>
        /// <returns> WaveFormat describing the same format </returns>
        private WaveFormat WaveFormatFromFmtChunk(FmtChunk fmt)
        {
            WaveFormat format = new WaveFormat();

            format.Channels = Convert.ToInt16(fmt.Channels);
            format.SamplesPerSecond = Convert.ToInt32(fmt.SampleRate);
            format.BitsPerSample = Convert.ToInt16(fmt.BitsPerSample);
            format.BlockAlign = Convert.ToInt16(fmt.FrameSize);
            format.AverageBytesPerSecond = Convert.ToInt32(fmt.AvgBytesPerSec);
            format.FormatTag = WaveFormatTag.Pcm; // fmt.Format;

            /*
            format.Channels = 1;
            format.BlockAlign = Convert.ToInt16(fmt.BitsPerSample/8);
            format.AverageBytesPerSecond = Convert.ToInt32(fmt.AvgBytesPerSec / fmt.Channels);*/

            return format;
        }

        /// <summary>
        ///  <see cref="EffectStream"/> used as direct input signal.
        /// </summary>
        private EffectStream input;

        /// <summary>
        ///  Gets or sets the play position.
        /// </summary>
        public int PlayPosition
        {
            get
            {
                if (stopped)
                    return input.Position;
                else
                    return input.Position - (bufferSizeSamples/2);
            }
            set
            {
                try
                {
                    input.Position = value;
                }
                catch (ArgumentException)
                { }
            }
        }

        /// <summary>
        ///  Gets the length of input file
        /// </summary>
        /// <returns> Length of the input file </returns>
        public int GetWaveLength()
        {
            return input.Length;
        }

        #endregion

        #region buffer_fields

        /// <summary>
        ///  SecondaryBuffer provides playing the buffer
        /// </summary>
        private SecondaryBuffer sBuffer;
        
        /// <summary>
        ///  Size of the buffer in bytes
        /// </summary>
        private int bufferSize;

        /// <summary>
        ///  Size of the buffer in samples.
        /// </summary>
        private int bufferSizeSamples;
        
        /// <summary>
        ///  position to write to the circular buffer for the next time
        /// </summary>
        private int nextWrite;
        
        /// <summary>
        ///  maximum latency that can occur when playing
        /// </summary>
        /// <remarks>
        ///  Filed doesn't store latency length itself (in miliseconds) but the corresponging amount of bytes according to Wave format
        /// </remarks>
        private int maxLatency;

        /// <summary>
        ///  Ensures that buffer is filled regularly
        /// </summary>
        private System.Timers.Timer timer;

        #endregion

        #region constructor

        /// <summary>
        ///  Creates new instance that plays given <see cref="Wave"/> and applies given effects on it.
        /// </summary>
        /// <param name="owner"> Owner of the player. Necessary to use DirectSound </param>
        /// <param name="wav"> Input file </param>
        /// <param name="effects"> Effects to apply. Number of boards must be greater or equal then channels count of the input file </param>
        public EffectStreamPlayer(Control owner,Wave.Wave wav,global::Effects.GUI.EffectBoard[] effects)
        {
            this.wav = wav;
            maxLatency = MSToBytes(20);

            // creat new sound device
            Device device = new Device();
            device.SetCooperativeLevel(owner, CooperativeLevel.Normal);

            // create format description
            BufferDescription descr = new BufferDescription(WaveFormatFromFmtChunk(wav.FmtChunk));
            descr.BufferBytes = Convert.ToInt32(wav.FmtChunk.AvgBytesPerSec/10);
            descr.ControlVolume = true;
            descr.GlobalFocus = true;

            // create new SecondaryBuffer
            sBuffer = new SecondaryBuffer(descr, device);
            //sBuffer = new SecondaryBuffer("pok3.wav", device);
            bufferSize = sBuffer.Caps.BufferBytes;
            bufferSizeSamples = sBuffer.Caps.BufferBytes / wav.FmtChunk.FrameSize;

            // create new timer
            //timer = new System.Timers.Timer(BytesToMS(bufferSize)/20);
            timer = new System.Timers.Timer(7);
            timer.Enabled = false;
            timer.Elapsed += new System.Timers.ElapsedEventHandler(this.TimerCall);

            input = new EffectStream(wav, effects, 5, bufferSize);
            input.Start();
            Stop();
        }

        #endregion

        #region commands
        // play, pause, stop commands

        /// <summary>
        ///  Indicates whether the player is stopped.
        /// </summary>
        bool stopped;

        /// <summary>
        ///  Stars playback. Fills buffer and starts playback.
        /// </summary>
        public void Play()
        {
            input.Start();
            if (stopped)
            {
                stopped = false;
                //System.Threading.Thread.Sleep(100);
                FeedFromPlayPosition(bufferSize/10);
                System.Threading.Thread.Sleep(100);
            }

            timer.Enabled = true;
            sBuffer.Play(0, BufferPlayFlags.Looping);
        }

        /// <summary>
        ///  Stops playback.
        /// </summary>
        public void Stop()
        {
            stopped = true;
            // stop timer from ticking
            if (timer != null)
                timer.Enabled = false;

            // stop Secondary Buffer            
            if (sBuffer != null)
            {
                sBuffer.Stop();
            }

            // reset position in udelying EffectStream
            input.Stop();
        }

        /// <summary>
        ///  Pauses the playback.
        /// </summary>
        public void Pause()
        {
            if (timer != null)
                timer.Enabled = false;
            if (sBuffer != null)
                sBuffer.Stop();

            input.Pause();
        }

        /// <summary>
        ///  Raised when player reaches end of the file.
        /// </summary>
        public event EventHandler EndOfFile;

        #endregion

        #region buffer_work

        /// <summary>
        ///  Gets amount that have been played since last feed
        /// </summary>
        /// <returns></returns>
        private int GetPlayedSize()
        {
            int pos = sBuffer.PlayPosition;
            return
               pos < nextWrite ?
               pos + bufferSize - nextWrite
               : pos - nextWrite;
        }
        
        /// <summary>
        ///  Fills the buffer. It gets data from input EffectStream and copies it to the circular buffer
        /// </summary>
        /// <param name="bytes"> Count of the bytes desired to put into the buffer </param>
        private void Feed(int bytes)
        {
            // limit latency
            int copyCount = Math.Min(bytes, maxLatency);

            if (copyCount > 0)
            {
                // restore buffer if needed
                if (sBuffer.Status.BufferLost)
                    sBuffer.Restore();

                // get data from stream
                byte[] getArray = input.Get(copyCount);
                try
                {
                    sBuffer.Write(nextWrite, getArray, LockFlag.None);

                }
                catch (ArgumentException)
                {
                    Console.WriteLine("Error");
                }

                // count write position for next feed
                nextWrite += copyCount;
                if (nextWrite > bufferSize)
                    nextWrite -= bufferSize;
            }
        }

        /// <summary>
        /// Fills the buffer from it's current position. It gets data from input EffectStream and copies it to the circular buffer
        /// </summary>
        /// <param name="bytes"></param>
        private void FeedFromPlayPosition(int bytes)
        {
            // limit latency
            int copyCount = Math.Min(bytes, maxLatency);

            if (copyCount > 0)
            {
                // restore buffer if needed
                if (sBuffer.Status.BufferLost)
                    sBuffer.Restore();

                // get data from stream
                byte[] getArray = input.Get(copyCount);
                int position = sBuffer.PlayPosition;

                try
                {

                    sBuffer.Write(position, getArray, LockFlag.None);

                }
                catch (ArgumentException)
                {
                    Console.WriteLine("Error");
                }

                // count write position for next feed
                nextWrite = position + copyCount;
                if (nextWrite > bufferSize)
                    nextWrite -= bufferSize;
            }
        }

        /// <summary>
        ///  Clears buffer.
        /// </summary>
        private void ClearBuffer()
        {
            // it fills circular buffer with zeroes
            if (sBuffer != null)
            {
                byte[] clear = new byte[sBuffer.Caps.BufferBytes];
                sBuffer.Write(0, clear, LockFlag.None);
            }

        }

        /// <summary>
        ///  Timer elapsed event handler. Feeds buffer.
        /// </summary>
        /// <param name="sender"> Object that has raised the event </param>
        /// <param name="e"> Event arguments </param>
        private void TimerCall(object sender, System.Timers.ElapsedEventArgs e)
        {
            // check the end
            if (input.ReadPosition == input.Length)
            {
                Stop();
                if (EndOfFile != null)
                    EndOfFile(this, new EventArgs());
            }

            int size = GetPlayedSize();
            Feed(size);
            //input.Buffer(size);
        }

        #endregion

        #region byte_ms_conversions
        // conversions between time in milisecs and byte counts

        /// <summary>
        ///  Converts byte count to miliseconds time interval according to the format of <see cref="EffectStreamPlayer.wav"/>
        /// </summary>
        /// <param name="bytes"> Number of bytes </param>
        /// <returns> Time interval in miliseconds </returns>
        private int BytesToMS(int bytes)
        {
            int oneKMilis= Convert.ToInt32(wav.FmtChunk.SampleRate * wav.FmtChunk.FrameSize);
            return (1000 * bytes) / oneKMilis;
        }

        /// <summary>
        ///  Converts miliseconds time interval to byte count according to the format of <see cref="EffectStreamPlayer.wav"/>
        /// </summary>
        /// <param name="ms"> Time interval in miliseconds </param>
        /// <returns> Number of the bytes </returns>
        private int MSToBytes(int ms)
        {
            int oneKMilis = Convert.ToInt32(wav.FmtChunk.SampleRate * wav.FmtChunk.FrameSize);
            return (ms * oneKMilis) / 1000;
        }

        #endregion
    }
}
